package sub3;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Observable;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;

import com.sun.org.apache.bcel.internal.generic.InstructionConstants.Clinit;

import exceptions.*;

/**
 * Keep the state of the client and handles the communication with the server
 * @author Magali Moulin, Pierre Labiausse
 * @version 3
 */

public class Client extends Observable {

	private String sharedFolderPath;
	private InetAddress mcastAddress;
	private int mcastPort, sharingPort;
	private LinkedList<String> sharedFiles = new LinkedList<String>();
	private HashMap<String, ArrayList<ClientTcpTuple>> searchResult = null;
	private String searchRequest = null;
	private MulticastSocket ms = null;	
	private long idRequest = -1;
	
	/**
	 * Getter to have access to port used by a client
	 * @return sharingPort
	 */
	public int getSharingPort(){
		return sharingPort;
	}
	
	/**
	 * Getter to have access to the id of the request. This id enables to identify a request
	 * @return idRequest
	 */
	public long getIdRequest() {
		return idRequest;
	}
	
	/**
	 * Setter to set an id for a request
	 * @param idRequest
	 */
	public void setIdRequest(long idRequest){
		this.idRequest = idRequest;
	}

	/**
	 * Getter to have access of the searched request.
	 * @return searchRequest: searched request by the client
	 */
	public String getSearchRequest() {
		return searchRequest;
	}
	
	/**
	 * Setter to put a new searched request 
	 * @param request: the searched request
	 */
	public void setSearchRequest(String request) {
		this.searchRequest = request;
	}

	/**
	 * Add a file and its corresponding clients in the HashMap searchResult 
	 * @param filename
	 * @param tuple
	 */
	public synchronized void  addSearchResult(String filename, ClientTcpTuple tuple){
		if(searchResult.keySet().contains(filename)){
			searchResult.get(filename).add(tuple);
		}
		ArrayList<ClientTcpTuple> _tmp = new ArrayList<ClientTcpTuple>();
		_tmp.add(tuple);
		searchResult.put(filename, _tmp);
	}
	
	/**
	 * Getter to have access of the HashMap searchResult when there is a research.
	 * @return searchResult: the HashMap searchResult
	 */
	public HashMap<String, ArrayList<ClientTcpTuple>> getSearchResult() {
		return searchResult;
	}

	/**
	 * Getter to have access of the list of shared files
	 * @return sharedFiles: list of shared files
	 */
	public LinkedList<String> getSharedFiles() {
		return sharedFiles;
	}

	private LinkedList<String> subdirectories = new LinkedList<String>();
	private boolean registered;

	/**
	 * Set the parameters to the default values 
	 */
	public Client(){
		sharedFolderPath = Configuration.DEFAULT_SHARE_FOLDER_PATH;
		try {
			mcastAddress = InetAddress.getByName(Configuration.DEFAULT_MCAST_ADDRESS);
			checkSharedFiles();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		mcastPort = Configuration.DEFAULT_PORT;
		sharingPort = 8081 + (new Random()).nextInt(100);
		//sharingPort = 8088;
		System.out.println("Sharing port : "+sharingPort);
		registered = false;
		setChanged();
		notifyObservers();
		(new SharingServer(sharingPort, this)).start();
	}

	private LinkedList<String> listFiles(){
		LinkedList<String> result = new LinkedList<String>();
		subdirectories.add(sharedFolderPath);
		while(subdirectories.size() != 0){
			listFiles(subdirectories.poll(), result);
		}
		return result;
	}
	private void listFiles(String folderFilename, LinkedList<String> result){
		File shareFolder = new File(folderFilename);
		for(String filename : shareFolder.list()){
			File _tmp = new File(folderFilename+"/"+filename);
			if( ! _tmp.isDirectory()){
				result.add(filename);
			}
			else {
				subdirectories.add(folderFilename+"/"+filename);
			}
		}
	}

	/**
	 *  Change the path to the share folder
	 *  @param newPath : the new path of the shared folder
	 *  @throws SharedFolderNotFoundException
	 */
	public void changeSharedFolderPath(String newPath) throws SharedFolderNotFoundException{
		File file = new File(newPath);
		if(! (file.exists() && file.isDirectory()) ){
			throw new SharedFolderNotFoundException();
		}
		sharedFolderPath = newPath;
		checkSharedFiles();
//		try {
//			share(serverAddress, serverPort);
//		} catch (FailureConnectionException e) {
//			e.printStackTrace();
//		}
		setChanged();
		notifyObservers();
	}
	
	/**
	 * Register the client with the server and send it the list of shared files
	 * Execute the Server part of the client, listening on the specified sahring port
	 * @param mcastAddress: the address of the multicast group used
	 * @param mcastPort : the port of the multicast group used
	 * @throws FailureConnectionException 
	 */
	public void share(InetAddress mcastAddress, int mcastPort) throws FailureConnectionException{
		// If it is not a multicast address, throw a FailureConnectionException
		if(!mcastAddress.isMulticastAddress()) throw new FailureConnectionException();
		
		// If we share already with another server, unshare with it
		if(isRegistered() && (!this.mcastAddress.equals(mcastAddress)||!(this.mcastPort != mcastPort) ) ){
			unshare();
		}
		this.mcastAddress = mcastAddress;
		this.mcastPort = mcastPort;
		try {
			ms = new MulticastSocket(mcastPort);
			ms.joinGroup(mcastAddress);
			(new RequestServer(ms, this)).start();
			registered = true;
			setChanged();
			notifyObservers();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Get the state of the client
	 * @return registered : registered or unregistered
	 */
	public boolean isRegistered() {
		return registered;
	}

	/**
	 * Unregister from the server
	 */
	public void unshare(){
		if(ms != null){
			try {
				ms.leaveGroup(mcastAddress);
				registered = false;
				ms.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Getter to have access of the address of the multicast group
	 * @return mcastAddress
	 */
	public InetAddress getMcastAddress() {
		return mcastAddress;
	}

	/**
	 * Getter to have access of the port of the multicast group
	 * @return mcastPort
	 */
	public int getMcastPort() {
		return mcastPort;
	}

	/**
	 * Getter to have the path of the shared folder
	 * @return sharedFolderPath: the path of the shared folder
	 */
	public String getSharedFolderPath() {
		return sharedFolderPath;
	}
	
	/**
	 *  Try to retrieve a file by contacting a peer 
	 * @param solFile: name of the file in the shared folder
	 * @param filename: name of the searched file
	 * @param peer: peer who must be contacted to retrieve the file
	 * @throws TestException
	 */
	public void retrieveFile(String filename, ClientTcpTuple peer, File solFile) throws TestException {
		Socket socket = null;
		PrintWriter out = null;
		ObjectInputStream in = null;
		
		// Connect to the peer and send him the filename
		try {
			System.out.println("Retrieve : "+filename+" from "+peer.getInetAddress().getHostAddress()+":"+peer.getPort());
			socket = new Socket(peer.getInetAddress(), peer.getPort());
			out = new PrintWriter(socket.getOutputStream());
			out.println("request");
			out.flush();
			out.println(filename);
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// Listen for the answer from the peer ("fileNotFound" || "fileFound")
		try {
			in = new ObjectInputStream(socket.getInputStream());
			String _response = null;
			while(_response == null){
				_response = (String)in.readObject();
			}
			if(_response.equals("fileNotFound")) {
				in.close();
				out.close();
				socket.close();
				throw new TestException();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
				e.printStackTrace();
		}

		// If the response was positive, receive the file and store it in the shared folder
		try {
			File file = (File)in.readObject();
			FileOutputStream outf = new FileOutputStream(solFile);
			FileInputStream inf = new FileInputStream(file);
			byte[] bytes = new byte[inf.available()];
			inf.read(bytes, 0, bytes.length);
			outf.write(bytes);
			outf.flush();
			outf.close();
			out.close();
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		// Check if we need to :
		// Update our entry in the server with our new file, reload the files shared displayed in the UI
		checkSharedFiles();
	}
	
	/**
	 *  Contact the peer for a search 
	 *  @param request:  the searched request
	 */
	public void searchRequest(String request){
		// If there is no request currently treated
		if(idRequest == -1){
			// Reset searchResult
			searchResult = new HashMap<String, ArrayList<ClientTcpTuple>>();
			searchRequest = request;

			// Send the request
			idRequest = System.currentTimeMillis();
			byte[] requestToSend = null;
			requestToSend = ("request "+idRequest+" "+sharingPort+" "+request).getBytes();
			DatagramPacket dp = new DatagramPacket(requestToSend, requestToSend.length, mcastAddress, mcastPort);
			try {
				System.out.println(new String(dp.getData()));
				ms.send(dp);
			} catch (IOException e) {
				e.printStackTrace();
			}

			// Put the timeout in place
			Timer t = new Timer();
			t.schedule(new TimerTask() {

				@Override
				public void run() {
					finishRequest();
				}
			}, 5000);

			//Listen for the responses
			DatagramSocket ds = null;
			try {
				ds = new DatagramSocket(sharingPort);
				ds.setSoTimeout(5000);
				while(idRequest != -1){
					DatagramPacket dp2 = new DatagramPacket(new byte[1024], 1024);
					try {
						ds.receive(dp2);
						String msg = null;
						msg = new String(dp2.getData(), 0, dp2.getLength());
						StringTokenizer st = new StringTokenizer(msg);
						String command = st.nextToken();
						String id = st.nextToken();
						if (command.equals("response") && id.equals(""+getIdRequest())){
							System.out.println("*************");
							int sharingPort = Integer.parseInt(st.nextToken());
							while(st.hasMoreTokens()){
								addSearchResult(st.nextToken(), new ClientTcpTuple(
										dp2.getAddress(), 
										sharingPort));
							}
							setChanged();
							notifyObservers();
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
					System.out.println("Received : "+new String(dp.getData(),0,dp.getLength()));
				}
			} catch (SocketException e) {
			}finally{ds.close();}
			System.out.println("Request done");
		}
	}
	
	protected void finishRequest() {
		if(searchResult.isEmpty()){
			System.out.println("Failure of the search");
		}
		idRequest = -1;
		setChanged();
		notifyObservers();
	}

	/**
	 *  Verify is the client needs to update its files in the view
	 */
	public void checkSharedFiles(){
		LinkedList<String> sharedFilesNow = listFiles();
		if(!sharedFilesNow.equals(sharedFiles)){
			sharedFiles = sharedFilesNow;
			setChanged();
			notifyObservers();
		}
	}
}

/**
 * Sharing server part of the client
 * 
 * @author Magali Moulin, Pierre Labiausse
 * @version 3
 *
 */
class SharingServer extends Thread {
	
	private Client client;
	private int sharingPort;
	
	/**
	 * Constructor of this class. 
	 * @param sharingPort: the own port of the client
	 * @param client: reference of the considered client.
	 */
	public SharingServer(int sharingPort, Client client){
		this.client = client;
		this.sharingPort = sharingPort;
	}

	@Override
	public void run() {
		ServerSocket sharingSocket = null;
		try {
			sharingSocket = new ServerSocket(sharingPort);
		} catch (IOException e) {
			e.printStackTrace();
		}
		while(true){
			try {
				Socket peerSocket = sharingSocket.accept();
				SharingHandler handler = new SharingHandler(peerSocket, client);
				handler.start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}	
}

/**
 * Request server part of the client, active while the client has joined a multicast group
 * 
 * @author Magali Moulin, Pierre Labiausse
 * @version 0.1
 *
 */
class RequestServer extends Thread {
	
	private Client client;
	private MulticastSocket ms;
	
	/**
	 * Constructor of this class. 
	 * @param ms: MulticastSocket used to the connections
	 * @param client: reference of the considered client.
	 */
	public RequestServer(MulticastSocket ms, Client client){
		this.client = client;
		this.ms = ms;
	}

	@Override
	public void run() {
		while(client.isRegistered()){
			try {
				System.out.println("Waiting for search request");
				DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
				ms.receive(dp);
				System.out.println("Received : "+new String(dp.getData(),0,dp.getLength()));
				RequestHandler handler = new RequestHandler(dp, client);
				handler.start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}	
}